Dwarves on Rails
Rails
is a web framework written in the
Ruby programming
language. It implements the
Model-View-Controller
architectural pattern. Several facets of Rails, including its use of
code generators and reliance on reading the database schema, make it ideal for
Agile
development.
Once again I will be using my Dwarf Manager test case. I have previously
used this test case with
Java JDBC,
Java EJB, and
C# .NET.
Getting Started
All files for this project are available
here. Directory paths on the
following indicate locations within the tar file.
The first step in creating a database-backed Rails application is the
creation of an initial database schema. My Postgresql files are
db/Dwarf_Schema.sql and
db/Dwarf_Load.sql. The table and
column names have been modified slightly from previous versions to
match Rails conventions. When names match Rails conventions little
configuration is necessary -- a major benefit compared to the lengthy
XML configuration files of Java EJB.
After an initial schema is completed the Rails generators can be run. I ran:
- rails dwarf
This created the initial directory structure and shared scripts
- ruby script/generate scaffold Dwarf
This created app/models/dwarf.rb,
app/controllers/dwarves_controller.rb,
and app/views/dwarves/list.rhtml (view->source),
_form.rhtml, edit.rhtml, new.rhtml, show.rhtml
- ruby script/generate scaffold Mountain
This created app/models/mountain.rb,
app/controllers/mountains_controller.rb,
and app/views/mountains/list.rhtml,
_form.rhtml, edit.rhtml, new.rhtml, show.rhtml
Models
Rails
ActiveRecord implements an
Object-
Relational Mapping layer. The generators initially created model
classes Dwarf and Mountain. Initially these
classes contain no predefined attributes -- instead Rails queries the
database at runtime to determine the available columns. Rails does not
retrive foreign key information, so the developer annotates the
generated classes with belongs_to, has_one, has_and_belongs_to_many, etc.
to indicate One-to-One, One-to-Many, and Many-to-Many associations. Note
ActiveRecord supports Many-to-Many assocations directly so join tables
like visits in the Data Model above are invisible to the developer.
The developer also never sees foreign keys; instead they can use
expressions like aDwarf.mountain.name to traverse to and access
desired fields directly.
Controllers
The generators initially produced controllers
DwarvesController and
MountainsController. These controllers are generated with
default method implementations
for index, list, show, new, create, edit, update, and destroy.
These actions are accessible from the web server. In the URL
http://localhost:3000/dwarves/edit/5
http://localhost:3000 identifies the application, dwarves
identifies the controller, edit identifies the action (method), and
5 is a parameter for the action (here the dwarf_id).
Views
The generators initially produced views
list.rhtml,
_form.rhtml, edit.rhtml, new.rhtml and show.rhtml for
both dwarves and mountains.
The views are .rhtml pages, which are html pages with embedded Ruby code
executed by the server (much like Java Server Pages, ASP, PHP, etc.).
The initially generated versions support the
basic CRUD (Create, Read, Update, Delete) actions. They are then
edited to support displaying and setting foreign key fields, etc.
After adding validation annotations to the model classes
(see Dwarf) the views will automatically return
validation errors:
AJAX
The Rails framework includes built-in support for AJAX
(Asynchronous JavaScript and XML).
I added an example of AJAX to the Listing dwarves page (the first
screen shot on this web page). When the visits hyperlink is
clicked it is replaced with the results of a query to find all the
mountains the dwarf has visited. This is done without reloading the
web page. Instead a XMLHttpRequest is made and the <div>
tag is replaced with the results. Rails provides a server-side ruby method
link_to_remote
plus a client-side javascript class
Ajax.Updater which does this automatically. In
dwarves list.rhtml it is implemented by line
<div id="div<%= dwarf.id %>"><%= link_to_remote("visits", :update => "div" + dwarf.id.to_s,
:url => { :action => :visits, :id => dwarf }) %></div>
In the client browser this looks like:
<div id="div3"><a href="#" onclick="new Ajax.Updater('div3', '/dwarves/visits/3',
{asynchronous:true, evalScripts:true}); return false;">visits</a></div>
When the hyperlink is clicked it invokes the (manually added)
visits action in
DwarvesController,
which returns the data via the (manually created) dwarves view
visits.rhtml.
SOAP Web Services
Rails Action Web Service
provides basic server-side support for SOAP and XML-RPC based web services.
The first step is once again generation:
The APIs in DwarfServiceApi are
annotated to indicate the parameter names and types and return types. Note
complex types such as arrays of Dwarf and Mountain objects can be used. Once
this is done the web server can return a WSDL document
describing the service to interested clients:
http://localhost:3000/dwarf_service/service.wsdl
returns service.wsdl.
The web service implementations are added to
DwarfServiceController.
The Ruby programming language includes a SOAP implementation which
dynamically retrieves and uses a WSDL document. With this it is easy
to construct a test client
soapTest.rb
which retrieves lists of dwarves and mountains.
Performance
By default Rails find(:all) methods implement the classic N+1 query
anti-pattern where one query is done to find all the rows of a base
table and then one query is done for each desired foreign-key-linked
row in other tables. Many object-relational mapping systems have
difficulties generating queries using joins. With a (often outer) join
most of these N+1 queries can be relaced with a single query.
Rails does provide a solution for this problem. find methods
accept an additional optional parameter :include which lists
the additional associations which should be preloaded when the base
query is performed. Outer joins will be generated. I modified
the list action on Mountain to use include:
def list
# @mountain_pages, @mountains = paginate :mountain, :per_page => 10
@mountains = Mountain.find(:all, :include => [:king])
end
Here is a Postgresql trace
before and
after.
Note we still see the queries to dynamically
determine the available columns in the table.
Rails also provides automatic transaction support. I have not
investigated it yet.